/*
 * Copyright 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.camera.camera2.internal;

import static androidx.camera.core.CameraUnavailableException.CAMERA_ERROR;

import android.content.Context;
import android.hardware.camera2.CameraDevice;
import android.media.CamcorderProfile;
import android.os.Build;
import android.util.Size;

import androidx.annotation.GuardedBy;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
import androidx.camera.camera2.impl.FeatureCombinationQueryImpl;
import androidx.camera.camera2.internal.compat.CameraManagerCompat;
import androidx.camera.core.CameraUnavailableException;
import androidx.camera.core.featuregroup.impl.FeatureCombinationQuery;
import androidx.camera.core.impl.AttachedSurfaceInfo;
import androidx.camera.core.impl.CameraDeviceSurfaceManager;
import androidx.camera.core.impl.CameraMode;
import androidx.camera.core.impl.CameraUpdateException;
import androidx.camera.core.impl.StreamUseCase;
import androidx.camera.core.impl.SurfaceConfig;
import androidx.camera.core.impl.SurfaceStreamSpecQueryResult;
import androidx.camera.core.impl.UseCaseConfig;
import androidx.core.util.Preconditions;

import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Camera device manager to provide the guaranteed supported stream capabilities related info for
 * all camera devices
 *
 * <p>{@link CameraDevice#createCaptureSession} defines the default
 * guaranteed stream combinations for different hardware level devices. It defines what combination
 * of surface configuration type and size pairs can be supported for different hardware level camera
 * devices. This structure is used to store the guaranteed supported stream capabilities related
 * info.
 */
public final class Camera2DeviceSurfaceManager implements CameraDeviceSurfaceManager {
    private static final String TAG = "Camera2DeviceSurfaceManager";
    private final Object mLock = new Object();
    @GuardedBy("mLock")
    private final Map<String, SupportedSurfaceCombination> mCameraSupportedSurfaceCombinationMap =
            new HashMap<>();
    private final CamcorderProfileHelper mCamcorderProfileHelper;
    private final CameraManagerCompat mCameraManager;
    private final Context mContext;

    /**
     * Creates a new, initialized Camera2DeviceSurfaceManager.
     */
    @RestrictTo(Scope.LIBRARY)
    public Camera2DeviceSurfaceManager(@NonNull Context context,
            @Nullable Object cameraManager, @NonNull Set<String> availableCameraIds)
            throws CameraUnavailableException {
        this(context, new CamcorderProfileHelper() {
            @Override
            public boolean hasProfile(int cameraId, int quality) {
                return CamcorderProfile.hasProfile(cameraId, quality);
            }

            @Override
            @SuppressWarnings("deprecation")
            public CamcorderProfile get(int cameraId, int quality) {
                return CamcorderProfile.get(cameraId, quality);
            }
        }, cameraManager, availableCameraIds);
    }

    Camera2DeviceSurfaceManager(@NonNull Context context,
            @NonNull CamcorderProfileHelper camcorderProfileHelper,
            @Nullable Object cameraManager,
            @NonNull Set<String> availableCameraIds)
            throws CameraUnavailableException {
        Preconditions.checkNotNull(camcorderProfileHelper);
        mCamcorderProfileHelper = camcorderProfileHelper;
        mContext = context;

        if (cameraManager instanceof CameraManagerCompat) {
            mCameraManager = (CameraManagerCompat) cameraManager;
        } else {
            mCameraManager = CameraManagerCompat.from(context);
        }

        try {
            onCamerasUpdated(new ArrayList<>(availableCameraIds)); // Initial population
        } catch (CameraUpdateException e) {
            if (e.getCause() instanceof CameraUnavailableException) {
                throw ((CameraUnavailableException) e.getCause());
            } else {
                throw new CameraUnavailableException(CAMERA_ERROR, e);
            }
        }
    }

    @Override
    public void onCamerasUpdated(@NonNull List<String> newCameraIds) throws CameraUpdateException {
        // === Stage 1: Pre-computation (outside the lock) ===
        Map<String, SupportedSurfaceCombination> newCombinations = new HashMap<>();
        Set<String> combinationsToCreate;
        synchronized (mLock) {
            combinationsToCreate = new HashSet<>(newCameraIds);
            combinationsToCreate.removeAll(mCameraSupportedSurfaceCombinationMap.keySet());
        }

        try {
            for (String cameraId : combinationsToCreate) {
                newCombinations.put(cameraId, createSurfaceCombination(cameraId));
            }
        } catch (CameraUnavailableException | RuntimeException e) {
            // If creation fails, throw our custom exception to trigger a rollback.
            throw new CameraUpdateException("Failed to create SupportedSurfaceCombination", e);
        }

        // === Stage 2: Commit (inside a brief lock) ===
        synchronized (mLock) {
            Map<String, SupportedSurfaceCombination> finalCombinations = new HashMap<>();
            for (String cameraId : newCameraIds) {
                if (mCameraSupportedSurfaceCombinationMap.containsKey(cameraId)) {
                    finalCombinations.put(cameraId,
                            mCameraSupportedSurfaceCombinationMap.get(cameraId));
                } else {
                    finalCombinations.put(cameraId, newCombinations.get(cameraId));
                }
            }
            mCameraSupportedSurfaceCombinationMap.clear();
            mCameraSupportedSurfaceCombinationMap.putAll(finalCombinations);
        }
    }

    /**
     * Prepare necessary resources for the surface manager.
     */
    private SupportedSurfaceCombination createSurfaceCombination(@NonNull String cameraId)
            throws CameraUnavailableException {
        FeatureCombinationQuery featureCombinationQuery =
                FeatureCombinationQuery.NO_OP_FEATURE_COMBINATION_QUERY;

        if (Build.VERSION.SDK_INT >= 35) {
            // TODO: b/417839748 - Decide on the appropriate API level for CameraX feature combo API
            featureCombinationQuery = new FeatureCombinationQueryImpl(mContext, cameraId,
                    mCameraManager);
        }

        return new SupportedSurfaceCombination(
                mContext, cameraId, mCameraManager, mCamcorderProfileHelper,
                featureCombinationQuery);
    }

    /**
     * Transform to a SurfaceConfig object with cameraId, image format and size info
     *
     * @param cameraMode    the working camera mode.
     * @param cameraId      the camera id of the camera device to transform the object
     * @param imageFormat   the image format info for the surface configuration object
     * @param size          the size info for the surface configuration object
     * @param streamUseCase the stream use case for the surface configuration object
     * @return new {@link SurfaceConfig} object
     * @throws IllegalArgumentException if the {@code cameraId} is not found in the supported
     *                                  combinations, or if there isn't a supported combination of
     *                                  surfaces available for the given parameters.
     */
    @Override
    public @NonNull SurfaceConfig transformSurfaceConfig(
            @CameraMode.Mode int cameraMode,
            @NonNull String cameraId,
            int imageFormat,
            @NonNull Size size,
            @NonNull StreamUseCase streamUseCase) {
        SupportedSurfaceCombination supportedSurfaceCombination;
        synchronized (mLock) {
            supportedSurfaceCombination =
                    mCameraSupportedSurfaceCombinationMap.get(cameraId);
        }

        Preconditions.checkArgument(supportedSurfaceCombination != null,
                "No such camera id in supported combination list: " + cameraId);

        return supportedSurfaceCombination.transformSurfaceConfig(
                cameraMode,
                imageFormat,
                size,
                streamUseCase);
    }

    @Override
    public @NonNull SurfaceStreamSpecQueryResult getSuggestedStreamSpecs(
            @CameraMode.Mode int cameraMode,
            @NonNull String cameraId,
            @NonNull List<AttachedSurfaceInfo> existingSurfaces,
            @NonNull Map<UseCaseConfig<?>, List<Size>> newUseCaseConfigsSupportedSizeMap,
            boolean isPreviewStabilizationOn,
            boolean hasVideoCapture,
            boolean isFeatureComboInvocation,
            boolean findMaxSupportedFrameRate) {
        Preconditions.checkArgument(!newUseCaseConfigsSupportedSizeMap.isEmpty(),
                "No new use cases to be bound.");

        SupportedSurfaceCombination supportedSurfaceCombination;

        synchronized (mLock) {
            supportedSurfaceCombination = mCameraSupportedSurfaceCombinationMap.get(cameraId);
        }

        Preconditions.checkArgument(supportedSurfaceCombination != null,
                "No such camera id in supported combination list: " + cameraId);

        return supportedSurfaceCombination.getSuggestedStreamSpecifications(
                cameraMode,
                existingSurfaces,
                newUseCaseConfigsSupportedSizeMap,
                isPreviewStabilizationOn,
                hasVideoCapture,
                isFeatureComboInvocation,
                findMaxSupportedFrameRate);
    }
}
